S02-01 面向对象-类和对象
[TOC]
概述
入门案例:养猫猫问题
养猫猫问题:张老太养了两只猫猫:一只名字叫小白,今年 3 岁,白色;另一只叫小花,今年 100 岁,花色。编写程序,当用户输入小猫的名字时,显示该猫的名字、年龄、颜色;若输入错误,显示“张老太没有这只猫猫”。
方案:现有技术
方式 1:单独定义变量
// 第1只猫信息
String cat1Name = "小白";
int cat1Age = 3;
String cat1Color = "白色";
// 第2只猫信息
String cat2Name = "小花";
int cat2Age = 100;
String cat2Color = "花色";缺点:不利于数据管理,变量过多易混乱。
方式 2:使用数组
// 第1只猫信息
String[] cat1 = {"小白", "3", "白色"};
// 第2只猫信息
String[] cat2 = {"小花", "100", "花色"};缺点:
- 数据类型不明确(年龄是字符串类型)
- 只能通过下标获取信息,对应关系不清晰
- 不能体现猫的行为(如叫、跑)
现有技术缺点
现有技术不利于数据管理、效率低,引出面向对象编程(OOP)。
方案:面向对象
实现思路:定义Cat类(自定义数据类型),包含属性(名字、年龄、颜色)和行为,通过创建对象管理每只猫的信息。
public class Object01 {
// 编写一个main方法
public static void main(String[] args) {
// 2. 实例化 Cat 类
// 实例化第一只猫(创建对象),并赋值给 cat1 变量
Cat cat1 = new Cat();
cat1.name = "小白";
cat1.age = 3;
cat1.color = "白色";
cat1.weight = 10;
// 实例化第二只猫,并赋值给 cat1 变量
Cat cat2 = new Cat();
cat2.name = "小花";
cat2.age = 100;
cat2.color = "花色";
cat2.weight = 20;
// 3. 访问对象属性
System.out.println("第1只猫信息:" + cat1.name + " " + cat1.age + " " + cat1.color + " " + cat1.weight);
System.out.println("第2只猫信息:" + cat2.name + " " + cat2.age + " " + cat2.color + " " + cat2.weight);
}
}
// 1. 定义猫类 Cat(自定义数据类型)
class Cat {
// 属性(成员变量)
String name; // 名字
int age; // 年龄
String color; // 颜色
double weight; // 体重
// 行为(后续补充)
}面向对象编程
面向对象编程(OOP,Object-Oriented Programming):是一种以对象为核心的编程范式,它将现实世界中的事物抽象为程序中的对象,通过封装对象的属性和行为、建立对象间的继承与多态关系,来组织和构建程序。
Java 是纯粹的面向对象语言(除基本数据类型外,所有事物都可抽象为对象,且基本类型也有对应的包装类),OOP 是 Java 的核心思想,其核心在于类与对象,并通过封装、继承、多态三大特性实现代码的高复用、易维护和可扩展。
类与对象
类(Class):对象的 “模板” 或 “蓝图”,是对一类具有相同 属性(状态) 和方法(行为) 的事物的抽象描述。
- 属性:也叫成员变量 / 字段(Field),表示对象的状态(比如人的姓名、年龄)。
- 方法:也叫成员方法(Method),表示对象的行为(比如人的吃饭、跑步)。
对象(Object):类的 “实例”,是类的具体实现,是内存中实际存在的实体。通过new关键字可以创建类的对象,每个对象都拥有类定义的属性和方法的独立副本。
类与对象的关系图
猫类(Cat)→ 提取共性:属性(name, age, color)、行为(run, cry...)
对象1(小白):name="小白",age=3,color="白色"
对象2(小花):name="小花",age=100,color="花色"对象内存图
对象在内容中的存在形式如图:

基础
属性
属性(成员变量,字段,Field):表示对象的状态(比如人的姓名、年龄)。
定义语法:
访问修饰符:public、protected、默认、private,用于控制属性的访问范围。
访问修饰符 属性类型 属性名;
// 示例
class Car {
protected String name;
}注意事项:
属性的类型:属性可以是基本数据类型(int、double)或引用类型(对象、数组)。
javaclass Car { // 基本类型 double price; // 引用类型 String name; String color; String[] masters; }属性的默认值:未赋值的属性有默认值,规则同数组:
- 基本类型:
int 0、short 0、byte 0、long 0float 0.0、double 0.0char \u0000boolean false
- 引用类型(如 String):
null
java// 属性的默认值 public class PropertiesDetail { public static void main(String[] args) { Person p1 = new Person(); System.out.println( "age=" + p1.age + " name=" + p1.name + " sal=" + p1.sal + " isPass=" + p1.isPass ); // age=0,name=null,sal=0.0,isPass=false } } class Person { int age; // 默认值:0 String name; // 默认值:null double sal; // 默认值:0.0 boolean isPass; // 默认值:false }- 基本类型:
对象创建
对象创建有以下 2 种方式:
方式 1:先声明再创建
javaCat cat; // 声明对象(栈中存储引用) cat = new Cat(); // 创建对象(堆中分配空间)方式 2:直接创建
javaCat cat = new Cat();
访问属性
语法:
对象名.属性名;示例:属性赋值/取值
cat.name = "小白"; // 赋值
System.out.println(cat.age); // 取值内存流程图:对象创建
示例代码:
Person p1 = new Person();
p1.age = 10;
p1.name = "小明";
Person p2 = p1; // p2指向p1引用的对象
System.out.println(p2.age); // 输出10(p1和p2操作同一个对象)创建对象的流程:
以Person p = new Person();为例:
- 加载
Person类信息(属性和方法,仅加载一次)。 - 在堆中分配空间,进行默认初始化(属性赋默认值)。
- 将堆中对象地址赋给栈中的引用变量
p。 - 进行指定初始化(如
p.name = "jack")。
内存流程图:
Person p1 = new Person();
p1.age = 10; p1.name = "小明";
Person p2 = p1;
System.out.println(p2.age);
练习:对象内存流程图
练习:画出以下代码的内存流程图
Person a = new Person();
a.age = 10;
a.name = "小明";
Person b;
b = a;
System.out.println(b.name); // "小明"
b.age = 200;
b = null;
System.out.println(a.age); // 200
System.out.println(b.age); // 抛出异常:java.lang.NullPointerException
匿名对象
匿名对象(Anonymous Object):就是创建对象时,没有把它的内存地址赋给任何一个变量(即没有给它起名字)。
普通对象 vs 匿名对象
语法对比:普通对象 vs 匿名对象:
我们先来看最直观的代码对比。假设有一个 Person 类,里面有一个 work() 方法。
1. 普通对象(有名对象):
// 创建对象,并把地址赋给变量 p (p 就是这个对象的名字)
Person p = new Person();
// 通过名字 p 去调用方法
p.work();
// 如果想再调用一次,还可以继续用 p
p.work();2. 匿名对象:
// 直接 new 出来,连名字都不起,直接跟着一个点(.)去调用方法
new Person().work();核心特征
匿名对象的核心特征:
只能使用一次,无法循环使用: 因为你没有用变量去记录它的内存地址,所以这行代码执行完之后,你就再也找不到这个对象了。如果你写了两行
new Person().work();,那是在内存里创建了两个完全不同的对象,分别调用了一次方法。生命周期极短(光速被回收):
由于匿名对象在使用完毕后没有任何变量指向它,它会立刻变成“内存垃圾”。Java 的垃圾回收器 (GC) 会在合适的时候迅速把它清理掉,这在一定程度上节省了内存。
常见使用场景
常见的使用场景:
在实际开发中,我们通常在以下三种场景下使用匿名对象,目的是为了让代码更简洁。
场景 1:只需要调用一次对象的方法时:
如果你创建一个对象仅仅是为了调用它里面的某一个方法,而且以后再也不用它了,写成匿名对象最省事。
// 传统写法:啰嗦
Scanner sc = new Scanner(System.in);
int num = sc.nextInt();
// 匿名对象写法:一气呵成
int num = new Scanner(System.in).nextInt();如果涉及到为对象赋值,不要使用匿名对象。

场景 2:作为方法的参数传递(最常用):
当你调用一个方法,这个方法需要接收一个对象作为参数,而你刚好只需要传进去,不需要在外面继续保留这个对象时。
class MathUtil {
public static void printInfo(Person p) {
p.work();
}
}
public class Main {
public static void main(String[] args) {
// 传统写法
Person p1 = new Person();
MathUtil.printInfo(p1);
// 匿名对象写法:直接把 new 出来的东西当作参数塞进去
MathUtil.printInfo(new Person());
}
}场景 3:作为方法的返回值:
当一个方法需要返回一个对象时,可以直接 return new 对象(),不用在方法里多定义一个变量。
public Person getPerson() {
// 传统写法
// Person p = new Person();
// return p;
// 匿名对象写法
return new Person();
}匿名对象 vs 匿名内部类
很多初学者会把匿名对象和匿名内部类 (Anonymous Inner Class) 搞混,它们名字很像,但完全不是一个东西:
- 匿名对象: 只是单纯的
new了一个已经存在的类,不给它分配变量名。(如上文所述)。 - 匿名内部类: 是在
new的同时,重写(或实现)了某个类/接口的方法。它不但没有变量名,连具体的类名都没有。
匿名内部类示例(对比用):
// 这是在创建一个没有名字的、实现了 Runnable 接口的子类对象
new Runnable() {
@Override
public void run() {
System.out.println("这是一个匿名内部类");
}
}.run();成员方法
方法(Method,成员方法):表示对象的行为(比如人的吃饭、跑步)。
快速入门
给 Person 类添加方法:
speak():输出“我是一个好人”cal01():计算 1+2+...+1000 的结果cal02(int n):计算 1+2+...+n 的结果getSum(int num1, int num2):计算两个数的和
public class Method01 {
public static void main(String[] args) {
// 创建Person对象
Person p1 = new Person();
// 调用方法
p1.speak(); // 输出"我是一个好人"
p1.cal01(); // 输出1+...+1000的结果
p1.cal02(5); // 输出1+...+5的结果
p1.cal02(10); // 输出1+...+10的结果
int sum = p1.getSum(10, 20); // 接收方法返回值
System.out.println("两数之和:" + sum);
}
}
class Person {
String name;
int age;
// 1. speak方法:无参数、无返回值
public void speak() {
System.out.println("我是一个好人");
}
// 2. cal01方法:无参数、无返回值
public void cal01() {
int res = 0;
for (int i = 1; i <= 1000; i++) {
res += i;
}
System.out.println("1+...+1000的结果:" + res);
}
// 3. cal02方法:有参数、无返回值
public void cal02( int n ) { // 形参
int res = 0;
for (int i = 1; i <= n; i++) {
res += i;
}
System.out.println("1+...+" + n + "的结果:" + res);
}
// 4. getSum方法:有参数、有返回值
public int getSum(int num1, int num2) { // 形参列表
return num1 + num2; // 返回值
}
}语法结构
语法格式
一个完整的 Java 成员方法由多个组成部分构成,语法格式如下:
[修饰符列表] [返回值类型] 方法名([参数列表]) [throws 异常类型列表] {
// 方法体:具体的功能逻辑代码
[return 返回值]
}下面对每个组成部分进行详细解析:
修饰符列表
修饰符分为访问修饰符和非访问修饰符,可组合使用(需遵循语法规则,比如abstract和static不能同时修饰一个方法)。
访问修饰符
| 修饰符 | 可见范围 | 说明 |
|---|---|---|
public | 所有类(跨包、跨类) | 最宽松的访问权限 |
protected | 本类、同包子类、不同包子类 | 继承相关的访问权限 |
default | 本类、同包类(无修饰符) | 默认访问权限 |
private | 仅本类 | 最严格的访问权限 |
非访问修饰符
| 修饰符 | 作用 |
|---|---|
static | 定义类方法(静态方法),属于类而非对象 |
final | 方法不能被子类重写(Override) |
abstract | 定义抽象方法,无方法体,仅声明方法签名(只能在抽象类/接口中) |
native | 本地方法,方法体由非 Java 代码(如 C/C++)实现,仅声明签名 |
synchronized | 同步方法,保证多线程环境下的原子性(避免线程安全问题) |
strictfp | 严格浮点计算,保证不同平台下浮点运算结果一致 |
返回值
核心特性:
方法执行完成后返回给调用者的结果类型,分为两种情况:
有返回值:
可以是 Java 的基本数据类型(
int/double等)、引用数据类型(String/Object/自定义类等)。此时方法体中必须通过
return语句返回对应类型(或类型兼容)的值,且return后不能有多余代码。


无返回值:用
void表示,方法体中可以省略return语句(或写return;表示结束方法)。java// 无返回值:仅打印信息 public void printInfo(String msg) { System.out.println(msg); // 可省略return,也可写return; }返回值数量:一个方法最多有一个返回值,如需返回多个结果,可使用数组


方法名
遵循 Java 的命名规范:
- 采用小驼峰命名法(首字母小写,后续单词首字母大写,如
eatFood、calculateSum)。 - 见名知意,准确描述方法的功能(如
getAge表示获取年龄,setName表示设置姓名)。 - 不能与关键字重名,且不能包含空格、特殊符号(除
_和$)。
参数列表
基本格式
基本格式:多个参数用逗号分隔,每个参数的格式为:
// 基本格式
参数类型 参数名
// 示例
int a, String b核心特性:
参数个数:一个方法可以有 0 个参数,也可以有多个参数,参数中间用
,逗号间隔。参数类型:参数可以为任意类型(包括基本类型和引用类型)。
形参/实参:
- 实参:调用方法时传递给形参的具体值。
- 形参:定义方法时的参数称为形参,
- 要求实参的类型、个数、顺序必须与形参兼容(自动类型转换除外,如
int可赋值给double)。
javaclass AA { // 1. 方法定义(形参) public int[] getSumAndSub(int n1, int n2) { // 形参 int[] resArr = new int[2]; resArr[0] = n1 + n2; resArr[1] = n1 - n2; return resArr; } } public class MethodDetail { public static void main(String[] args) { // 2. 方法调用(实参) AA a = new AA();// ✅ 兼容:int -> int a.getSumAndSub(10, 4); // 实参 // ✅ 兼容:byte -> int byte b1 = 1, b2 = 2; a.getSumAndSub(b1, b2); // 实参 // ❌ 不兼容:double -> int a.getSumAndSub(1.1, 1.2); // 实参 } }
可变参数(不定长参数)
JDK 5 引入的特性,允许方法接收任意个数的同类型参数,语法为:参数类型... 参数名(本质是数组)。
- 可变参数必须放在参数列表的最后一位。
- 一个方法只能有一个可变参数。
示例:
// 可变参数:计算多个int的和
public int sum(int... nums) {
int total = 0;
for (int num : nums) { // 可变参数可当作数组遍历
total += num;
}
return total;
}
// 调用:可传入任意个数的int
sum(1, 2); // 结果3
sum(1, 2, 3, 4); // 结果10异常声明(throws)
声明方法可能抛出的受检异常(Checked Exception),告诉调用者需要处理这些异常(要么try-catch捕获,要么继续throws)。
示例:
// 声明方法可能抛出IOException
public void readFile(String path) throws IOException {
// 读取文件的逻辑,可能抛出IOException
}方法体
方法的核心逻辑代码块,包含变量定义、语句执行、流程控制(分支、循环)、调用其他方法等。
核心特性:
访问成员/局部变量:
方法体中可以访问类的成员变量(实例变量/静态变量)和局部变量(方法内定义的变量)。
局部变量必须先声明并初始化才能使用,而成员变量有默认值(如
int默认 0,String默认null)。
方法不能嵌套定义(不同于 JS)
javaclass Demo { // ❌ 方法不能嵌套定义 public void fn1() { public void fn2() {} } }
内存流程图:方法调用
示例代码:
public static void main(String[] args) {
Person p1 = new Person();
int sum = p1.getSum(10, 20);
System.out.println("两数之和:" + sum);
}
class Person {
public int getSum(int num1, int num2) {
return num1 + num2;
}
}方法调用流程:
- 执行方法时,JVM 会开辟独立的栈空间(方法栈)。
- 方法执行完毕或遇到
return时,栈空间释放,返回调用处。 - 返回后继续执行方法后面的代码。
- 当 main 方法(栈)执行完毕后,整个程序退出。
内存流程图:

成员方法作用
成员方法作用:
- 提高代码复用性:避免重复编写相同逻辑。
- 封装实现细节:用户无需关心内部逻辑,直接调用。
示例:封装二维数组遍历方法
定义MyTools类的printArr方法,实现二维数组遍历:
public class Method02 {
public static void main(String[] args) {
int[][] map = {{0,0,1},{1,1,1},{1,1,3}};
// 2. 多次调用方法,复用代码
MyTools tool = new MyTools();
tool.printArr(map);
tool.printArr(map);
}
}
class MyTools {
// 1. 封装方法:遍历二维数组
public void printArr(int[][] map) {
for (int i = 0; i < map.length; i++) {
for (int j = 0; j < map[i].length; j++) {
System.out.print(map[i][j] + "\t");
}
System.out.println();
}
}
}方法调用
方法调用注意事项:
本类中方法调用:直接调用(如
sayOk()调用print())。javaclass A { public void print(int n) { System.out.println("print方法被调用,n=" + n); } public void sayOk() { print(10); // 本类中调用:直接调用 } }跨类方法调用:通过对象名调用(如
B类对象调用A类方法)。javaclass A { public void m1() { // 跨类调用:通过B类对象名调用 B b = new B(); b.hi(); } } class B { public void hi() { System.out.println("B类的hi方法被执行"); } }注意:访问修饰符
跨类的方法调用和方法的访问呢修饰符也有关系。
练习
练习 1:判断奇偶方法
编写
AA类的isOdd(int num)方法:判断一个数是奇数还是偶数,返回boolean。java// 定义方法:判断奇偶 public boolean isOdd(int num) { return num % 2 != 0; } // 调用方法 System.out.println(a.isOdd(5)); // true System.out.println(a.isOdd(4)); // false练习 2:打印图形方法
编写
AA类的print(int row, int col, char c)方法:根据行、列、字符,打印对应格式的图形。java// 定义方法:打印图形 public void print(int row, int col, char c) { for (int i = 0; i < row; i++) { for (int j = 0; j < col; j++) { System.out.print(c); } System.out.println(); // 换行 } } // 调用方法 a.print(4, 4, '#');
方法传参机制@
Java 的方法传参机制是面试高频考点,也是理解方法调用底层逻辑的核心。核心结论:Java 中只有一种传参方式 —— 值传递(Pass by Value),不存在 “引用传递(Pass by Reference)”。很多开发者误以为引用类型是 “引用传递”,本质是对 “值” 的定义理解偏差:引用类型传递的是 “对象引用地址的副本”,而非引用本身,这仍属于值传递范畴。
值传递的本质
值传递的核心:
调用方法时,JVM 会创建实参的副本,并将这个副本传递给方法的形参;方法内部对形参的所有修改,仅作用于副本,不会直接影响原始实参。
对比容易混淆的 “引用传递”(Java 不支持):
引用传递是将实参的内存地址直接传递给形参,形参和实参指向同一个内存位置,方法内修改形参会直接改变实参。
基本数据类型
底层原理:
基本数据类型(int/double/boolean等)的实参存储在栈内存中,传参时会复制实参的数值本身给形参:
形参是栈中新创建的局部变量,与实参完全独立;
方法内修改形参的数值,仅改变栈中副本,原实参不受影响。
代码示例 + 内存解析:
public class BasicTypeParam {
// 方法:修改形参a的值
public static void changeInt(int a) {
a = 10; // 仅修改栈中形参副本
System.out.println("方法内a的值:" + a); // 输出:10
}
public static void main(String[] args) {
int num = 5; // 实参num:栈中存储值5
changeInt(num); // 传递num的副本(5)给形参a
System.out.println("方法外num的值:" + num); // 输出:5(原实参未变)
}
}内存执行流程(文字图解):
main方法执行时,栈中创建变量num,赋值为5(栈地址:比如0x001,值:5);- 调用
changeInt(num)时,JVM 在栈中为形参a分配新空间(栈地址:0x002),将num的5复制给a; - 方法内执行
a=10,仅修改0x002地址的值为10,0x001地址的num仍为5; - 方法执行完毕,形参
a出栈销毁,main方法中num的值保持5。
引用数据类型
底层原理:
引用数据类型(自定义类 / 数组 / 集合 / String 等)的实参存储逻辑:
栈内存:存储对象的引用地址(指向堆内存中的实际对象);
堆内存:存储对象的属性 / 数据。
传参时,JVM 复制的是栈中的引用地址副本给形参:
形参和实参的地址副本指向同一个堆对象;
方法内通过地址副本修改堆对象的属性:会影响原对象(因为指向同一个堆内存);
方法内修改形参的地址指向(比如
new新对象):仅修改副本的地址,原实参的地址不变,原对象不受影响。

场景 1:修改堆对象的属性(影响原对象):
// 自定义引用类型
class Person {
String name;
int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
}
public class RefTypeParam1 {
// 方法:修改对象的age属性
public static void changePersonAttr(Person p) {
p.age = 20; // 通过地址副本修改堆对象属性
System.out.println("方法内p的age:" + p.age); // 输出:20
}
public static void main(String[] args) {
// 实参person:栈存地址0x003,堆存{name:"张三", age:18}
Person person = new Person("张三", 18);
changePersonAttr(person); // 传递地址副本0x003给形参p
System.out.println("方法外person的age:" + person.age); // 输出:20(原对象被修改)
}
}内存执行流程:
main中创建Person对象:栈中person存地址0x003,堆中0x003地址存储{name:"张三", age:18};- 调用方法时,形参
p得到地址副本0x003,此时person和p都指向堆中同一个对象; - 方法内
p.age=20:直接修改堆中0x003地址的age属性,原person的age同步变化; - 方法结束后,
p出栈,person仍指向0x003,age为20。
场景 2:修改引用地址的指向(不影响原对象):
public class RefTypeParam2 {
// 方法:修改形参p的地址指向
public static void changePersonRef(Person p) {
// 形参p指向新的堆对象(地址0x004)
p = new Person("李四", 25);
System.out.println("方法内p的name:" + p.name); // 输出:李四
}
public static void main(String[] args) {
Person person = new Person("张三", 18); // 指向0x003
changePersonRef(person); // 传递0x003给p
System.out.println("方法外person的name:" + person.name); // 输出:张三(原对象未变)
}
}内存执行流程:
main中person指向堆地址0x003(张三,18);- 方法调用时,
p得到0x003的副本,此时p和person都指向0x003; - 方法内
p = new Person(...):p的地址改为0x004(李四,25),但person仍指向0x003; - 方法结束后,
p出栈,person的地址和指向的对象均未变化。
不可变类型
String、Integer、Double等包装类属于不可变类型:对象一旦创建,内部属性(如String的value数组、Integer的value)无法修改,所有 “修改” 操作本质是创建新对象。
String 传参示例:
public class StringParam {
public static void changeString(String s) {
s = "李四"; // 不是修改原对象,而是创建新String对象(地址0x005)
System.out.println("方法内s的值:" + s); // 输出:李四
}
public static void main(String[] args) {
String str = "张三"; // 栈中str指向常量池地址0x004("张三")
changeString(str); // 传递0x004给s
System.out.println("方法外str的值:" + str); // 输出:张三(原对象未变)
}
}Integer 包装类传参示例:
public class IntegerParam {
public static void changeInteger(Integer i) {
i = 20; // 自动装箱,创建新Integer对象(地址0x006)
System.out.println("方法内i的值:" + i); // 输出:20
}
public static void main(String[] args) {
Integer num = 10; // 指向常量池地址0x005(10)
changeInteger(num);
System.out.println("方法外num的值:" + num); // 输出:10
}
}核心原因:
不可变类型的 “修改” 本质是重新赋值,即让形参指向新对象,但原实参的地址仍指向旧对象,因此无法影响原实参。
命令行参数
概述
你一定在 Java 中写过无数次 psvm,并且生成过下面这行代码:
public static void main(String[] args) { ... }你有没有好奇过,括号里的 String[] args 到底是干什么用的?
其实,这就是 命令行参数 (Command-Line Arguments)。它允许你在启动程序的那一瞬间,从外部向程序内部传递一些信息,而不需要修改代码并重新编译。
核心本质:普通的字符串数组
- 本质:它就是一个普通的字符串数组 (
String Array)。 - 名字:
args只是arguments(参数)的缩写。你完全可以把它改成String[] myData,程序一样能跑,只是业界习惯叫args。 - 来源:当你通过命令行执行
java 类名时,跟在类名后面的内容,就会被 Java 虚拟机 (JVM) 自动收集起来,塞进这个数组里。
接收命令行参数
代码实战:如何接收参数:
假设我们写了一个简单的程序,用来给指定的用户发送问候语。
public class Greeting {
public static void main(String[] args) {
// 1. 先检查有没有传参数进来
if (args.length == 0) {
System.out.println("请在启动时提供用户名!");
return; // 结束程序
}
// 2. 获取第一个参数 (数组索引从 0 开始)
String username = args[0];
System.out.println("你好," + username + "!欢迎学习 Java。");
// 3. 打印所有参数看看
for (String arg : args) {
System.out.println("-> " + arg);
}
System.out.println("你一共传入了 " + args.length + " 个参数:");
}
}传递命令行参数
场景 A:终端
场景 A:在真实命令行/终端中运行:
这是最原汁原味的方式。编译好 .class 文件后,在黑框框(终端)里输入:
- 输入命令:
java Greeting 张三 李四 王五 - 解析规则:Java 会以空格作为分隔符。
- 最终结果:
args[0]是"张三"args[1]是"李四"args[2]是"王五"
场景 B:IDEA
场景 B:在 IntelliJ IDEA 中运行 (开发测试时):
在 IDEA 里,我们通常直接点绿色三角运行,没有机会敲命令行。这时候需要配置:
在代码编辑区右上角,找到当前运行的类名,点击旁边的下拉箭头。
选择 Edit Configurations... (编辑配置)。
在弹出的窗口中,找到 Program arguments (程序参数) 这一栏。
在里面输入你的参数:
张三 李四 王五(中间用空格隔开)。点击 OK 保存,然后再次运行程序即可。

注意事项
坑 1:参数里本身就有空格怎么办:
如果你的参数是一个完整的文件路径,比如 C:\Program Files\Java。如果你直接传,Java 会把它当成两个参数:C:\Program 和 Files\Java。
- 解决办法:用双引号把整个参数包起来。
- 正确传法:
java MyApp "C:\Program Files\Java"
坑 2:忘了转换数据类型:
不管你在命令行输入的是不是数字,Java 接收到的 永远都是字符串 (String)。如果你输入了 java Calculator 10 20,你想把它们相加,直接写 args[0] + args[1] 会得到 "1020"(字符串拼接)。
- 解决办法:必须手动将字符串解析为其他类型。
- 代码示例:
int num1 = Integer.parseInt(args[0]);
坑 3:越界异常 (ArrayIndexOutOfBoundsException):
这是新手最容易犯的错误。你直接在代码里写了 System.out.println(args[0]);,但是启动程序的时候忘记传参数了。此时 args 是个空数组(长度为 0),去拿第 0 个元素就会导致程序崩溃。
- 解决办法:永远在处理参数前,先判断
args.length > 0。
访问修饰符
Java 提供 4 种访问修饰符(Modifier),其访问范围与包密切相关。以下是类和成员的访问权限对比表:
类的访问修饰符
类只能用 public 或 default(无修饰符)修饰,访问范围如下:
| 修饰符 | 同包可见 | 不同包可见 | 核心说明 |
|---|---|---|---|
public | ✅ | ✅ | 所有包可访问,一个 .java 文件只能有一个 public 类,且类名与文件名一致 |
default | ✅ | ❌ | 仅同包可访问,无修饰符的类属于默认访问权限 |
类成员的访问修饰符
类的成员(变量、方法、内部类)可使用 4 种修饰符,访问范围如下:
| 修饰符 | 本类 | 同包 | 不同包子类 | 所有包 | 核心说明 |
|---|---|---|---|---|---|
private | ✅ | ❌ | ❌ | ❌ | 仅本类可访问,与包无关 |
default | ✅ | ✅ | ❌ | ❌ | 仅同包可访问,无修饰符 |
protected | ✅ | ✅ | ✅ | ❌ | 同包 + 不同包子类可访问 |
public | ✅ | ✅ | ✅ | ✅ | 所有包可访问 |
示例:
示例 1:protected 跨包访问
protected修饰的成员,不同包的子类只能通过子类实例访问,不能通过父类实例访问。java// 父类:com.demo.parent package com.demo.parent; public class Parent { protected String name = "Parent"; }java// 子类:com.demo.child(不同包) package com.demo.child; import com.demo.parent.Parent; public class Child extends Parent { public void test() { // 1. ✅ 合法:子类实例访问 protected 成员 System.out.println(this.name); // Parent // 2. ✅ 合法:子类实例访问 Child child = new Child(); System.out.println(child.name); // 3. ❌ 错误:不同包不能通过父类实例访问 protected 成员 Parent parent = new Parent(); // System.out.println(parent.name); } }示例 2:四种访问修饰符的访问范围
javapackage com.hspedu.modifier; public class A { // 修饰属性 public int n1 = 100; protected int n2 = 200; int n3 = 300; private int n4 = 400; // 修饰方法 public void m1() {} protected void m2() {} void m3() {} private void m4() {} public void hi() { // 1. ✅ 同类可访问所有属性和方法 System.out.println(n1 + " " + n2 + " " + n3 + " " + n4); m1(); m2(); m3(); m4(); } }java// 同包: package com.hspedu.modifier; public class B { public void say() { A a = new A(); // 2. ✅ 同包可访问:public/protected/default属性和方法 System.out.println(a.n1 + " " + a.n2 + " " + a.n3); a.m1(); a.m2(); a.m3(); // 3. ❌ 同包类不可访问:private属性和方法 // System.out.println(a.n4); // a.m4(); } }java// 不同包: package com.hspedu.pkg; public class C { public void say() { A a = new A(); // 2. ✅ 不同包可访问:public属性和方法 System.out.println(a.n1); a.m1(); // 3. ❌ 不同包不可访问:protected/default/private属性和方法 // System.out.println(a.n2 + " " + a.n3 + "" + a.n4); // a.m2(); a.m3();a.m4(); } }
方法重载
概述
方法重载(Method Overload):在同一个类中,存在多个方法满足以下条件:
- 方法名称完全相同;
- 参数列表必须不同(参数个数、参数类型、参数顺序,三者满足其一即可);
- 与参数名、返回值类型、访问修饰符、抛出异常列表无关。
本质:编译时静态绑定:
Java 编译器在编译阶段(而非运行阶段),会根据调用方法时传入的实参的静态类型(声明类型),匹配对应的重载方法并确定调用目标,这个过程称为 “静态绑定”。
方法重写(Override):是运行时动态绑定,根据对象的实际类型确定调用方法。
重载的核心价值:
- 语义统一:同一类操作(如 “加法”“打印”)用同一个方法名,符合 “见名知意” 的编码习惯;
- 简化调用:调用者无需记忆不同功能的方法名,仅需传入不同参数即可;
- 适配多场景:支持同一逻辑适配不同输入参数(如计算不同个数的数值和、处理不同类型的数据源)。
快速入门
public class OverLoad01 {
// 1. 两个int的和
public int calculate(int n1, int n2) {
System.out.println("调用:两个int的和");
return n1 + n2;
}
// 2. int+double的和
public double calculate(int n1, double n2) {
System.out.println("调用:int+double的和");
return n1 + n2;
}
// 3. double+int的和
public double calculate(double n1, int n2) {
System.out.println("调用:double+int的和");
return n1 + n2;
}
// 4. 三个int的和
public int calculate(int n1, int n2, int n3) {
System.out.println("调用:三个int的和");
return n1 + n2 + n3;
}
public static void main(String[] args) {
System.out.println(calculate(1, 2)); // 调用两个int参数的方法
System.out.println(calculate(1, 2.1)); // 调用int+double的方法
System.out.println(calculate(1.1, 2)); // 调用double+int的方法
System.out.println(calculate(1, 2, 3)); // 调用三个int参数的方法
}
}编译器匹配优先级
编译器在匹配重载方法时,遵循 “精确匹配 → 自动类型转换匹配 → 可变参数匹配” 的优先级,逐级匹配,直到找到唯一匹配的方法;若匹配到多个或无匹配,均会报错。
优先级 1:精确匹配(最优先)
实参类型与形参类型完全一致,直接匹配:
javapublic class OverloadMatch { public static void print(int a) { System.out.println("int类型:" + a); } public static void print(double a) { System.out.println("double类型:" + a); } public static void main(String[] args) { print(10); // 精确匹配print(int) → int类型:10 print(10.5); // 精确匹配print(double) → double类型:10.5 } }优先级 2:自动类型转换匹配
若无精确匹配,编译器会尝试自动向上转型(如
int → long → float → double、char → int)匹配:注意:若存在多个可转换的匹配(如同时有
long和float重载),编译器会报错(模糊的方法调用)。javapublic class OverloadMatch { public static void print(long a) { System.out.println("long类型:" + a); } public static void print(double a) { System.out.println("double类型:" + a); } public static void main(String[] args) { print(10); // int无精确匹配,自动转long → long类型:10 print('a');// char无精确匹配,自动转int→再转long → long类型:97 } }优先级 3:可变参数匹配(最低)
仅当精确匹配、自动转换匹配均失败时,才会匹配可变参数(...)的重载方法:
public class OverloadMatch {
public static void print(int... nums) {
System.out.println("可变参数:" + Arrays.toString(nums));
}
public static void main(String[] args) {
print(10, 20); // 无精确匹配,匹配可变参数 → 可变参数:[10, 20]
}
}练习
练习 1:判断题
与 `void show(int a, char b, double c)` 构成重载的有(b c d e):
- a) void show(int x, char y, double z){} // 否(形参列表完全相同)
- b) int show(int a, double c, char b){} // 是(形参顺序不同)
- c) void show(int a, double c, char b){} // 是(形参顺序不同)
- d) boolean show(int c, char b){} // 是(形参个数不同)
- e) void show(double c){} // 是(形参个数、类型不同)
- f) double show(int x, char y, double z){} // 否(形参列表完全相同)
- g) void shows(){} // 否(方法名不同)练习 2:重载方法实现
定义
Methods类的 3 个重载方法m:m(int n):输出 n 的平方。m(int n1, int n2):输出 n1×n2 的结果。m(String str):输出字符串。
javaclass Methods { // 1. 定义 m 方法的重载 public void m(int n) { System.out.println("平方=" + (n * n)); }public void m(int n1, int n2) { System.out.println("相乘=" + (n1 * n2)); } public void m(String str) { System.out.println("传入的str=" + str); } } public class OverLoadExercise { public static void main(String[] args) { Methods method = new Methods(); // 2. 测试 m 方法 method.m(10); // 输出:平方=100 method.m(10, 20); // 输出:相乘=200 method.m("韩顺平教育hello"); // 输出:传入的str=韩顺平教育hello } } 定义 3 个重载方法
max:max(int n1, int n2):返回两个 int 的最大值。max(double n1, double n2):返回两个 double 的最大值。max(double n1, double n2, double n3):返回三个 double 的最大值。
javaclass Methods { // 1. 定义 max 方法的重载 public int max(int n1, int n2) { return n1 > n2 ? n1 : n2; }public double max(double n1, double n2) { return n1 > n2 ? n1 : n2; } public double max(double n1, double n2, double n3) { double max1 = n1 > n2 ? n1 : n2; return max1 > n3 ? max1 : n3; } } public class OverLoadExercise { public static void main(String[] args) { Methods method = new Methods(); // 2. 测试 max 方法 System.out.println(method.max(10, 24)); // 24 System.out.println(method.max(10.0, 21.4)); // 21.4 System.out.println(method.max(10.0, 1.4, 30)); // 30.0 } }
可变参数
概述
可变参数(Variable Arguments,Varargs):是方法参数的一种特殊形式,通过 类型名... 参数名 声明,允许调用方法时传入 0 个、1 个或多个同类型参数,编译器会自动将这些参数封装为一个数组,方法内部可直接将可变参数当作数组处理。
核心本质:数组的 “语法糖”
可变参数并非新的参数类型,而是 JDK 为简化数组参数调用设计的语法糖 —— 编译阶段,编译器会将 类型... 参数名 转换为 类型[] 参数名,方法调用时传入的零散参数会被自动打包为数组。
反编译后显示为数组:

语法格式
可变参数声明在方法参数列表中,格式为:
[修饰符] 返回值类型 方法名([固定参数列表], 类型... 可变参数名) {
// 方法体:可变参数当作数组处理
}快速入门
实现计算任意个数的 int 类型之和。
public class VarParameter01 {
// 可变参数sum:接收任意个数的int,使用时可以当做数组来用
public int sum(int... nums) {
int res = 0;
for (int i = 0; i < nums.length; i++) {
res += nums[i];
}
return res;
}
public static void main(String[] args) {
System.out.println(sum(1, 5, 100)); // 106(3个数之和)
System.out.println(sum(1, 19)); // 20(2个数之和)
System.out.println(sum()); // 0(0个数之和)
}
}核心使用规则(必守)
核心使用规则(必守):
规则 1:可变参数必须是参数列表的最后一个
可变参数接收 “任意数量” 的参数,若后面还有其他参数,编译器无法区分可变参数的结束位置,因此编译报错:
javapublic class VarargsRule1 { // ✅ 正确:可变参数在最后 public static void print(String prefix, int... nums) {} // ❌ 错误:可变参数后有其他参数(编译报错:Varargs parameter must be the last parameter) public static void print(int... nums, String suffix) {} }规则 2:一个方法只能有一个可变参数
同理,多个可变参数会导致编译器无法拆分参数边界,编译报错:
javapublic class VarargsRule2 { // ❌ 错误:一个方法多个可变参数(编译报错:Variable arity parameter must be the last parameter) public static void sum(int... nums1, double... nums2) {} }规则 3:可变参数可接收 0 个参数
调用时不传可变参数,编译器会传入一个空数组(而非
null),方法内部遍历不会报空指针:javapublic class VarargsRule3 { public static void print(int... nums) { System.out.println("数组长度:" + nums.length); // 0个参数时输出0 for (int num : nums) { // 遍历空数组,无异常 System.out.println(num); } } public static void main(String[] args) { print(); // 数组长度:0 } }规则 4:可变参数支持基本类型和引用类型
- 基本类型可变参数:
int...、double...等; - 引用类型可变参数:
String...、Person...等。
javaclass Person { private String name; public Person(String name) { this.name = name; } } public class VarargsType { // 引用类型可变参数 public static void printPersons(Person... persons) { for (Person p : persons) { System.out.println(p.name); } } public static void main(String[] args) { printPersons(new Person("张三"), new Person("李四")); } }- 基本类型可变参数:
规则 5:可直接传递数组给可变参数
编译器兼容数组参数,无需手动拆分,等价于传入多个零散参数:
javapublic class VarargsArray { public static int sum(int... nums) { int total = 0; for (int num : nums) total += num; return total; } public static void main(String[] args) { int[] arr = {1,2,3}; // 直接传数组,等价于sum(1,2,3) System.out.println(sum(arr)); // 6 } }
底层实现原理
JVM 本身不支持可变参数,其实现完全依赖编译器的 “语法糖转换”,核心步骤:
- 编译方法时:将
类型... 参数名替换为类型[] 参数名,方法签名变为方法名(类型[]); - 编译方法调用时:
- 若传入零散参数(如
sum(1,2,3)),自动打包为数组new int[]{1,2,3}; - 若传入数组(如
sum(arr)),直接传递数组引用,不做额外处理; - 若传入 0 个参数,自动创建空数组
new int[0]。
- 若传入零散参数(如
反编译验证(使用 javap -c 命令):
编译 sum(int... nums) 后,字节码中方法签名为 sum([I)I([I 表示 int 数组),与 sum(int[] nums) 完全一致。
练习
封装一个可变参数方法showScore,接收姓名和任意门课成绩,返回“姓名+课程数+总分”。
public class VarParameterExercise {
public String showScore(String name, double... scores) {
double totalScore = 0;
for (int i = 0; i < scores.length; i++) {
totalScore += scores[i];
}
return name + " 有" + scores.length + "门课,总分为:" + totalScore;
}
public static void main(String[] args) {
System.out.println(showScore("milan", 90.1, 80.0));
System.out.println(showScore("terry", 90.1, 80.0, 10, 30.5, 70));
}
}作用域
概述
作用域(Scope):是 Java 中程序元素(变量、方法、类、接口等)的可见范围和生命周期的集合,核心作用是控制元素的访问权限和内存管理。其中,变量的作用域是最核心、最易混淆的部分(方法 / 类的作用域主要由访问修饰符决定)。
作用域的关键维度:
- 可见性(Visibility):程序的哪个部分能访问该元素(如 “方法内的变量仅方法内可见”)
- 生命周期(Lifecycle):元素的创建 / 销毁时机(如 “局部变量随方法调用创建,调用结束销毁”)
作用域分类
作用域的核心分类(按范围从小到大):
块级作用域 < 方法/构造器作用域 < 类级作用域(成员变量) < 包级作用域 < 全局作用域(public类/方法)块级作用域
块级作用域(Block Scope):在代码块({} 包裹的区域)内声明的变量,包括:
- 条件块(
if/else、switch); - 循环块(
for、while、do-while); - 同步块(
synchronized); - 普通代码块(直接用
{}包裹的代码)。
核心特性:
- 生命周期:进入块时在栈内存创建,退出块时立即销毁(栈帧弹出)
- 可见性:仅在当前块及嵌套的子块内可见,块外无法访问
- 默认值:无默认值,必须显式初始化后才能使用(否则编译报错)
- 命名规则:同一块内不能声明同名变量;块嵌套时,内层变量可覆盖外层同名变量(就近原则)
示例:
普通块作用域
javapublic class BlockScopeDemo { public static void main(String[] args) { // 外层块 { int a = 10; // 块级变量,作用域:外层块 System.out.println(a); // 合法:10 // 嵌套块 { int b = 20; // 作用域:嵌套块 System.out.println(a + b); // 合法:30(内层可访问外层) int a = 30; // 合法:内层覆盖外层同名变量(就近原则) System.out.println(a); // 30(访问内层a) } // System.out.println(b); // 错误:b仅在嵌套块内可见 } // System.out.println(a); // 错误:a仅在外层块内可见 } }循环 / 条件块作用域
javapublic class BlockScopeDemo2 { public static void main(String[] args) { // for循环块:i的作用域仅在循环内 for (int i = 0; i < 3; i++) { System.out.println(i); // 合法 } // System.out.println(i); // 错误:i超出作用域 // if块:flag的作用域仅在if内 if (true) { boolean flag = true; System.out.println(flag); // 合法 } // System.out.println(flag); // 错误:flag超出作用域 } }
方法/构造器作用域
方法 / 构造器作用域(Method/Constructor Scope):在方法 / 构造器内声明的局部变量(包括方法的形参),是块级作用域的 “超集”(方法体本身就是一个大代码块)。
核心特性:
- 生命周期:方法 / 构造器调用时创建(栈帧压入),执行完毕后销毁(栈帧弹出)
- 可见性:整个方法 / 构造器内可见(包括内部嵌套块),方法外无法访问
- 默认值:无默认值,必须显式初始化(形参由调用者传值初始化)
- 命名规则:方法内局部变量不能与形参同名;方法内嵌套块可覆盖方法级变量
示例:方法局部变量 + 形参
public class MethodScopeDemo {
public static int add(int a, int b) { // 1. 方法形参(属于方法作用域)
int sum = a + b; // 2. 方法级局部变量
if (sum > 10) {
int temp = sum * 2; // 嵌套块变量,仅if内可见
sum = temp;
}
// System.out.println(temp); // 错误:temp超出作用域
return sum;
}
public static void main(String[] args) {
int result = add(3, 5);
// System.out.println(a); // 错误:形参a超出方法作用域
// System.out.println(sum); // 错误:sum超出方法作用域
}
}类级作用域(成员变量)
类级作用域(成员变量,属性):在类内、方法 / 块外声明的变量(类的成员),分为两类:
- 实例变量(非静态)
- 静态变量(类变量)
实例变量
实例变量(Instance Variable)特性:
- 修饰符:无
static修饰,可加private/public/protected等访问修饰符 - 生命周期:对象创建时在堆内存分配,对象被 GC 回收时销毁
- 可见性:整个类内可见;外部需通过对象实例访问(
obj.var),受访问修饰符限制 - 默认值:有默认值(同数组规则:int→0,String→null,boolean→false 等)
- 线程安全:非线程安全(每个对象有独立副本)
示例:实例变量
public class ClassScopeDemo {
// 1. 实例变量(对象级,每个对象独立)
private String name;
private int age; // 默认值0
public ClassScopeDemo(String name, int age) {
this.name = name;
this.age = age;
}
public void show() {
// 2. 类内可直接访问成员变量
System.out.println("姓名:" + name + ",年龄:" + age);
}
public static void main(String[] args) {
// 3. 类外:必须通过对象访问实例变量
ClassScopeDemo obj1 = new ClassScopeDemo("张三", 20);
ClassScopeDemo obj2 = new ClassScopeDemo("李四", 25);
obj1.show(); // 姓名:张三,年龄:20
obj2.show(); // 姓名:李四,年龄:25
}
}静态变量
静态变量(Class Variable)特性:
- 修饰符:
static修饰,可加访问修饰符 - 生命周期:类加载时在方法区分配,类卸载(JVM 退出)时销毁
- 可见性:整个类内可见;外部可通过类名 / 对象实例访问(
Class.var),受访问修饰符限制 - 默认值:有默认值(同数组规则:int→0,String→null,boolean→false 等)
- 线程安全:线程不安全(所有对象共享同一个副本)
示例:静态变量
public class ClassScopeDemo {
// 1. 静态变量(类级,所有对象共享)
private static String className = "Java基础";
public void show() {
// 2. 类内可直接访问成员变量(与实例变量相同)
System.out.println("类名:" + className);
}
public static void main(String[] args) {
// 3. 类外静态变量:通过类名访问(推荐)
System.out.println(ClassScopeDemo.className); // Java基础
// 4. 静态变量共享:修改后所有对象可见
ClassScopeDemo.className = "Java进阶";
obj1.show(); // 类名:Java进阶
obj2.show(); // 类名:Java进阶
}
}异常参数作用域
异常参数作用域(Catch Scope):catch 块中声明的异常参数(如 catch (Exception e)),属于特殊的块级作用域。
核心特性:
- 生命周期:
catch块执行时创建,执行完毕后销毁 - 可见性:仅在当前
catch块内可见 - 命名规则:同一
try-catch的多个catch块可声明同名异常参数(不同作用域)
示例:catch 块异常参数
public class CatchScopeDemo {
public static void main(String[] args) {
try {
int a = 1 / 0;
} catch (ArithmeticException e) {
// e仅在当前catch块内可见
System.out.println("算术异常:" + e.getMessage());
} catch (Exception e) {
// 不同catch块,同名e合法
System.out.println("通用异常:" + e.getMessage());
}
// System.out.println(e); // 错误:e超出catch作用域
}
}内部类作用域
内部类的作用域依赖其定义位置,分为:
- 成员内部类
- 局部内部类
- 匿名内部类
成员内部类
成员内部类:类内、方法外的内部类;
可见性:可访问外部类的所有成员(包括 private);
外部类访问内部类:需通过内部类实例(Outer.Inner inner = new Outer().new Inner())。
示例:成员内部类
public class InnerClassScopeDemo {
private String outerVar = "外部类变量";
// 1. 成员内部类
class MemberInner {
public void show() {
// 可访问外部类private成员
System.out.println(outerVar); // 外部类变量
}
}
public static void main(String[] args) {
InnerClassScopeDemo outer = new InnerClassScopeDemo();
// 2. 成员内部类:外部需通过实例访问
MemberInner memberInner = outer.new MemberInner();
memberInner.show();
}
}局部内部类
局部内部类:方法 / 块内的内部类;
可见性:仅在方法 / 块内可见;
访问规则:可访问外部类的成员,以及外部方法的 final/effectively final 局部变量(JDK 8+)。
示例:局部内部类
public class InnerClassScopeDemo {
private String outerVar = "外部类变量";
// 方法内局部内部类
public void testLocalInner() {
// effectively final变量(未重新赋值)
String localVar = "方法局部变量";
// 1. 局部内部类
class LocalInner {
public void show() {
System.out.println(outerVar); // 访问外部类成员
System.out.println(localVar); // 访问外部方法final变量
}
}
// 2. 局部内部类仅方法内可见
LocalInner inner = new LocalInner();
inner.show();
}
public static void main(String[] args) {
InnerClassScopeDemo outer = new InnerClassScopeDemo();
outer.testLocalInner();
}
}匿名内部类
匿名内部类:无类名的局部内部类;
作用域:与局部内部类一致,仅在定义的方法 / 块内有效;
访问规则:同局部内部类。
作用域规则总结
作用域规则总结:
默认值:
- 块级/方法/构造器:没有默认值,必须显式初始化后才能使用(否则编译报错)
- 类级(实例/静态):有默认值,规则与数组一致(int→0,String→null,boolean→false 等)
变量重名:
同一块内:不能声明同名变量;
javapublic void hi() { String address = "北京"; // String address = "上海"; // ❌ 错误(同一作用域局部变量重名) }块嵌套时:内层变量可覆盖外层同名变量(就近原则)
javaclass Person { String name = "jack"; public void say() { String name = "king"; // 局部变量与属性重名 System.out.println("say() name=" + name); // 就近原则,输出king } }
生命周期:
- 类级(实例/静态):伴随对象创建而创建,伴随对象销毁而销毁。
- 块级/方法/构造器:伴随代码块执行而创建,伴随代码块结束而销毁。
javaclass Person { // 1. 类级变量 name 是随着 Person 对象的创建而创建,随着 Person 对象的销毁而销毁(持续时间更久) String name = "jack"; public void say() { // 2. 局部变量 name 会随着 say() 调用而创建,也会随着 say() 调用结束而销毁 String name = "king"; } }访问范围:
- 类级(实例/静态):整个类内可见;外部需通过类名 / 对象实例访问(
Class.var/obj.var),受访问修饰符限制。 - 块级/方法/构造器:整个块 / 方法 / 构造器内可见(包括内部嵌套块),块 / 方法外无法访问。
javaclass Person { String name = "jack"; public void say() { String age = "king"; } public void hi() { System.out.println(age); // ❌ 2. 访问失败 } } class T { public void test() { Person p1 = new Person(); System.out.println(p1.name); // ✅ 1. 其他类通过对象访问属性,输出jack } }- 类级(实例/静态):整个类内可见;外部需通过类名 / 对象实例访问(
修饰符:
- 类级(实例/静态):可加
private/public/protected等访问修饰符。 - 块级/方法/构造器:不能加访问修饰符。
javaclass Person { // 1. 属性可以添加修饰符 public String name = "jack"; public void say() { // 2. 局部变量不可以添加修饰符 // public String age = "king"; } }- 类级(实例/静态):可加
this
概述
this:是 Java 保留关键字,有两种核心语义:
- 实例上下文:在实例方法 / 构造方法中,
this指代调用当前方法的对象实例(或正在初始化的对象实例),可通过this访问对象的成员变量 / 方法; - 构造调用:在构造方法中,
this(参数)用于调用本类的其他构造方法,实现构造逻辑复用。
本质:实例方法的隐式参数
Java 中所有实例方法(非 static 方法)都会隐式接收一个 this 参数,该参数由 JVM 在调用方法时自动传递,指向调用此方法的对象实例。
示例:字节码编译验证
从字节码层面看,实例方法的第一个参数永远是 this(类型为当前类),比如:
// 源码
public class Person {
private String name;
public void setName(String name) {
this.name = name;
}
}
// 编译后的字节码(简化):setName方法的第一个参数是this
// void setName(LPerson; Ljava/lang/String;)VJVM 调用 person.setName("张三") 时,会将 person 对象的引用作为 this 参数传递给 setName 方法,因此方法内可通过 this 访问该对象的 name 成员。

使用场景
this 的核心使用场景:
场景 1:区分同名的局部变量(形参)与成员变量(属性)
这是
this最常用的场景:当方法的局部变量与类的成员变量同名时,this.成员变量明确指向成员变量,避免 “变量遮蔽”。javapublic class Person { private String name; private int age; // 形参name/age与成员变量同名,用this区分 public Person(String name, int age) { // this.name:成员变量;name:形参 this.name = name; this.age = age; } }场景 2:调用本类的实例方法
this.方法名()可显式调用本类的其他实例方法,虽然省略this也能调用,但显式使用可增强代码可读性(尤其是区分继承的方法时)。javapublic class Calculator { private int result; public void add(int num) { this.result += num; } public void multiply(int num) { // 显式调用本类的add方法(省略this也可,但this增强语义) this.add(num); this.result *= num; } }场景 3:调用本类的其他构造方法
构造方法中通过
this(参数)调用本类的重载构造方法,实现构造逻辑复用,核心规则:this()必须是构造方法的第一条语句;this()的参数列表需匹配本类的某个构造方法;- 不能递归调用(如构造 A 调
this(),而this()又调回构造 A); - 不能与
super()同时使用(二者都需是第一条语句)。 this()语法只能在构造器中使用,不能在其他成员方法中使用。
javapublic class User { private String username; private String password; private String email; // 两参构造:调用三参构造,email传默认值 public User(String username, String password) { this(username, password, "default@xxx.com"); // 必须在第一行 System.out.println("两参构造执行"); } // 核心三参构造:初始化所有成员 public User(String username, String password, String email) { this.username = username; this.password = password; this.email = email; System.out.println("三参构造执行"); } public static void main(String[] args) { User u2 = new User("李四", "654321"); // 输出顺序: // 三参构造执行 → 两参构造执行 } }场景 4:返回当前对象(链式调用)
在实例方法中返回
this(当前对象引用),可实现 “链式调用”(如 Builder 模式、流式编程),简化代码调用。javapublic class StringBuilderDemo { private StringBuilder sb = new StringBuilder(); // 返回this,支持链式调用 public StringBuilderDemo append(String str) { sb.append(str); return this; // 返回当前对象 } public StringBuilderDemo append(int num) { sb.append(num); return this; } public String getResult() { return sb.toString(); } public static void main(String[] args) { // 链式调用:连续调用append方法 String result = new StringBuilderDemo() .append("Hello") .append("Java") .append(2025) .getResult(); System.out.println(result); // HelloJava2025 } }场景 5:将当前对象作为参数传递
this可作为实参传递给其他方法,将当前对象引用传递出去,适用于 “对象协作” 场景(如回调、依赖注入)。java// 工具类:接收Person对象并打印信息 class PersonUtil { public static void printPerson(Person person) { System.out.println("姓名:" + person.getName() + ",年龄:" + person.getAge()); } } public class Person { private String name; private int age; public Person(String name, int age) { this.name = name; this.age = age; } public void showInfo() { // 将当前对象(this)传递给PersonUtil的printPerson方法 PersonUtil.printPerson(this); } // getter public String getName() { return this.name; } public int getAge() { return this.age; } public static void main(String[] args) { Person p = new Person("王五", 25); p.showInfo(); // 输出:姓名:王五,年龄:25 } }场景 6:内部类中访问外部类的 this
非静态内部类(成员内部类 / 局部内部类)有自己的
this(指向内部类实例),若需访问外部类的this,需通过外部类名.this显式指定。javapublic class Outer { private String outerVar = "外部类变量"; // 成员内部类 class Inner { private String innerVar = "内部类变量"; private String outerVar = "内部类覆盖的outerVar"; public void show() { // 1. this:指向内部类实例 System.out.println(this.innerVar); // 内部类变量 System.out.println(this.outerVar); // 内部类覆盖的outerVar // 2. Outer.this:指向外部类实例 System.out.println(Outer.this.outerVar); // 外部类变量 } } public static void main(String[] args) { Outer outer = new Outer(); Outer.Inner inner = outer.new Inner(); inner.show(); } }
核心规则
this 的核心规则(必守):
规则 1:不能在静态上下文使用 this
静态上下文(静态方法、静态代码块、静态内部类)属于类级别,无对象实例,因此
this无指向,编译直接报错。javapublic class ThisStaticError { private static String staticVar = "静态变量"; private String instanceVar = "实例变量"; // 1. 静态方法:不能使用this public static void staticMethod() { // System.out.println(this.instanceVar); // 错误:Cannot use this in a static context // System.out.println(this); // 错误:同上 } // 2. 静态代码块:不能使用this static { // System.out.println(this.staticVar); // 错误:同上 } // 3. 静态内部类:不能访问外部类的this static class StaticInner { public void show() { // System.out.println(ThisStaticError.this.instanceVar); // 错误:静态内部类无外部this } } }规则 2:this () 调用构造方法的限制
this()必须是构造方法的第一条语句,否则编译报错;this()不能递归调用(如public Person() { this(); }会无限递归,编译报错);this()的参数列表必须匹配本类的某个构造方法(参数个数 / 类型 / 顺序一致);- 不能同时使用
this()和super()(二者都需是第一条语句,冲突)。
javapublic class ThisConstructorRule { public ThisConstructorRule() { // System.out.println("非第一条语句"); // 错误:this()必须是第一条 this(10); } public ThisConstructorRule(int num) { // this(); // 错误:递归调用构造方法 } // public ThisConstructorRule(String str) { // super(); // 错误:不能同时用super()和this() // this(10); // } }规则 3:this 指向调用方法的对象实例
同一个类的不同对象调用同一实例方法,
this指向不同的实例,保证每个对象的成员独立。可以通过.hashCode()判断 this 指向不同的内存地址。javapublic class ThisPointDemo { private int num; public void showThis() { System.out.println("this的引用地址:" + this.hashCode()); } public static void main(String[] args) { ThisPointDemo obj1 = new ThisPointDemo(); ThisPointDemo obj2 = new ThisPointDemo(); obj1.showThis(); // this指向obj1(地址不同) obj2.showThis(); // this指向obj2(地址不同) } }规则 4:this 不能被赋值
this是关键字,不是可赋值的变量,试图给this赋值会编译报错:javapublic class ThisAssignError { public void assignThis() { // 错误:The left-hand side of an assignment must be a variable // this = new ThisAssignError(); } }规则 5:this 永远不为 null(合法调用下)
实例方法必须通过对象调用(
obj.method()),JVM 会将obj作为this传递,因此合法调用下this不可能为null。例外:通过反射非法调用实例方法时,可传递
null作为this,此时方法内使用this会抛出NullPointerException:javaimport java.lang.reflect.Method; public class ThisNullReflect { public void show() { System.out.println(this); // 反射传null时,this为null → NPE } public static void main(String[] args) throws Exception { Method method = ThisNullReflect.class.getMethod("show"); method.invoke(null); // 传递null作为this → 运行时NPE } }
练习
需求:定义 Person 类,提供compareTo()方法,判断两个 Person 对象的名字和年龄是否完全一致。
public class TestPerson {
public static void main(String[] args) {
Person p1 = new Person("mary", 20);
Person p2 = new Person("mary", 20);
System.out.println("p1 和 p2 比较的结果:" + p1.compareTo(p2)); // true
}
}
class Person {
String name;
int age;
// 构造器
public Person(String name, int age) {
this.name = name;
this.age = age;
}
// 比较方法
public boolean compareTo(Person p) {
// this 代表当前对象(调用compareTo方法的对象)
return this.name.equals(p.name) && this.age == p.age;
}
}本章作业
- 编写类 A01,定义方法
max(),实现求某个 double 数组的最大值,并返回(Homework01.java) - 编写类 A02,定义方法
find(),实现查找某字符串是否在字符串数组中,并返回索引;找不到返回-1(Homework02.java) - 编写类 Book,定义方法
updatePrice(),更改书的价格:- 价格 >150 → 150
- 价格 >100 → 100
- 否则不变(Homework03.java)
- 编写类 A03,实现数组复制功能
copyArr(),输入旧数组,返回新数组(元素和旧数组一致)(Homework04.java) - 定义圆类 Circle,属性:半径;提供方法:显示圆周长、显示圆面积(Homework05.java)
- 创建 Calc 计算类,定义两个操作数,提供加减乘除方法(除数为 0 时提示),创建对象测试(Homework06.java)
- 设计 Dog 类,属性:名字、颜色、年龄;定义
show()方法显示信息(使用 this.属性)(Homework07.java) - 以下代码编译运行后,输出结果是(
10,9,10)javapublic class Test { int count = 9; public void count1() { count = 10; System.out.println("count1=" + count); } public void count2() { System.out.println("count1=" + count++); } public static void main(String args[]) { new Test().count1(); // count1=10 Test t1 = new Test(); t1.count2(); // count1=9(后置++,先输出后自增) t1.count2(); // count1=10 } } - 定义 Music 类,属性:音乐名 name、时长 times;方法:播放 play()、返回属性信息 getInfo()(Homework09.java)
- 以下代码运行结果是(
101,100,101,101)javaclass Demo { int i = 100; public void m() { int j = i++; // j=100,i=101 System.out.println("i=" + i); // 101 System.out.println("j=" + j); // 100 } } class Test { public static void main(String[] args) { Demo d1 = new Demo(); Demo d2 = d1; // 引用传递,指向同一个对象 d2.m(); System.out.println(d1.i); // 101 System.out.println(d2.i); // 101 } } - 调用语句
System.out.println(method(method(10.0,20.0),100));编译正确,method方法的定义形式为:javapublic double method(double d1, double d2) { ... } - 创建 Employee 类,属性:名字、性别、年龄、职位、薪水;提供 3 个构造器(复用构造器):
- 构造器 1:初始化所有属性
- 构造器 2:初始化名字、性别、年龄
- 构造器 3:初始化职位、薪水(Homework12.java)
- 将对象作为参数传递给方法(Homework13.java)
- 定义 Circle 类:属性 radius,方法
findArea()返回面积 - 定义 PassObject 类:方法
printAreas(Circle c, int times),打印 1~times 的半径及对应面积 - 测试类中调用
printAreas(),输出结果如下:Radius 1.0 → Area 3.141592653589793 Radius 2.0 → Area 12.566370614359172 Radius 3.0 → Area 28.274333882308138 Radius 4.0 → Area 50.26548245743669 Radius 5.0 → Area 78.53981633974483
- 定义 Circle 类:属性 radius,方法
- 扩展题:设计 Tom 类,实现与电脑猜拳功能(石头 0、剪刀 1、布 2),记录输赢次数(Homework14.java)
包
概述
多个程序员开发同一个项目时,可能出现类名冲突(如两个 Dog 类),通过包区分。
包(Package):是 Java 中管理类和接口的核心机制,本质是命名空间(Namespace),用于解决类名冲突、组织代码结构、控制访问权限三大核心问题。它是 Java 模块化编程的基础,贯穿于类的定义、导入、编译、运行全流程。每个类都属于一个包,若未显式声明包,则属于默认包(无名包)。
包的本质:命名空间与代码组织
- 命名空间:通过包名给类名添加前缀,避免不同包下的类名冲突(如
java.util.Date和java.sql.Date); - 代码组织:按功能模块划分包,使代码结构清晰,便于维护(如
com.demo.controller存放控制器、com.demo.service存放服务类)。
包的命名规则
Java 官方规定包名采用反向域名命名法,确保全球唯一:
- 只能包含数字/字母/下划线/点
.,不能以数字开头,不能有关键字/保留字; - 全部小写,避免大小写敏感问题;
- 以公司 / 组织的反向域名开头,后接功能模块;
- 不同层级用点
.分隔。
// 公司项目
package com.baidu.xxx
// 个人项目
package com.demo.controller
package com.demo.service基本语法
包的声明
包的声明语法格式:
package 包名;核心规则:
- 必须是源文件的第一条语句:在
package语句之前不能有任何代码(可以有注释); - 一个源文件只能有一个 package 语句:一个类只能属于一个包;
- 包名与目录结构一致:编译后,包名对应操作系统的目录结构(如
com.demo对应com/demo目录)。
示例:
// 正确:package 是第一条语句
package com.demo.model;
public class User {
private String name;
}默认包
若源文件中未显式声明 package 语句,则该类属于默认包(无名包)。
默认包特点:
- 包名为空,类名无前缀;
- 不同源文件的默认包类之间可直接访问(默认访问权限);
- 不推荐使用:易导致类名冲突,且无法导入其他包的类(JDK 1.4 后限制)。
示例:
// 无 package 语句,属于默认包
public class DefaultPackageClass {}快速入门
// 包com.xiaoming下的Dog类
package com.xiaoming;
public class Dog {}// 包com.xiaoqiang下的Dog类
package com.xiaoqiang;
public class Dog {}// 测试类
package com.use;
import com.xiaoming.Dog; // 导入指定包的类
public class Test {
public static void main(String[] args) {
Dog dog1 = new Dog(); // com.xiaoming.Dog
com.xiaoqiang.Dog dog2 = new com.xiaoqiang.Dog(); // 全类名访问
}
}三大核心作用
包的三大核心作用:
作用 1:解决类名冲突(核心作用)
不同包下的类可以同名,通过全限定类名(包名 + 类名)区分。
java// 1. 导入 java.util 包的 Date 类 import java.util.Date; // 2. 导入 java.sql 包的 Date 类,用别名区分 import java.sql.Date as SqlDate; public class DateDemo { public static void main(String[] args) { Date utilDate = new Date(); // java.util.Date java.sql.Date sqlDate = new java.sql.Date(System.currentTimeMillis()); // 全限定类名 } }作用 2:组织代码结构(模块化编程)
按功能模块划分包,使代码结构清晰,符合 “高内聚、低耦合” 的设计原则。
plaintextcom.demo ├── controller // 控制器(处理请求) ├── service // 服务层(业务逻辑) ├── dao // 数据访问层(操作数据库) ├── model // 实体类(数据模型) ├── util // 工具类(通用功能) └── config // 配置类(全局配置)作用 3:控制访问权限(与访问修饰符配合)
包与访问修饰符(
default、protected)配合,实现类的访问控制:- default 访问权限(无修饰符):仅同包内的类可访问;
- protected 访问权限:同包内的类 + 不同包的子类可访问。
java// 包 com.demo.a package com.demo.a; // default 权限的类:仅同包可访问 class DefaultClass { public void show() { System.out.println("default 类的方法"); } } public class TestA { public static void main(String[] args) { // 同包的测试类:可访问 DefaultClass DefaultClass dc = new DefaultClass(); // 合法 dc.show(); } }java// 不同包的测试类:不可访问 DefaultClass package com.demo.b; import com.demo.a.DefaultClass; // 错误:DefaultClass 是 default 权限,无法导入 public class TestB { public static void main(String[] args) { // DefaultClass dc = new DefaultClass(); // 错误:无法访问 } }
类的导入
通过 import 语句引入其他包的类,避免在代码中重复写全限定类名,简化代码编写。
语法格式
语法格式:
格式 1:导入单个类
javaimport 包名.类名;格式 2:导入包下所有类(通配符
*)javaimport 包名.*;格式 3:静态导入(导入类的静态成员)
java// 导入单个静态成员 import static 包名.类名.静态成员名; // 导入所有静态成员 import static 包名.类名.*;
核心规则:
import 语句的位置:必须在
package语句之后,类定义之前;一个源文件可有多条 import 语句:按需导入多个包的类;
通配符
*仅导入当前包的类:不导入子包的类(如import com.demo.*不导入com.demo.controller.User);默认导入:JVM 自动导入
java.lang.*包下的所有类(如String、System、Object),无需显式导入。
示例:
单个类导入与全限定类名
javapackage com.demo.test; // 1. 导入 java.util 包的 ArrayList 类 import java.util.ArrayList; public class ImportDemo { public static void main(String[] args) { // 2. 简化写法:直接使用类名 ArrayList<String> list1 = new ArrayList<>(); // 全限定类名:不导入也可使用,但繁琐 java.util.HashMap<String, String> map = new java.util.HashMap<>(); } }通配符导入
javapackage com.demo.test; // 1. 导入 java.util 包下所有类 import java.util.*; public class ImportWildcardDemo { public static void main(String[] args) { ArrayList<String> list = new ArrayList<>(); // 2. 合法 HashMap<String, String> map = new HashMap<>(); // 3. 合法 } }静态导入
javapackage com.demo.test; // 1. 静态导入 Math 类的 PI 常量和 sqrt 方法 import static java.lang.Math.PI; import static java.lang.Math.sqrt; public class StaticImportDemo { public static void main(String[] args) { // 2. 简化写法:直接使用静态成员 System.out.println(PI); // 3.141592653589793 System.out.println(sqrt(4)); // 2.0 // 3. 不静态导入的写法:Math.PI、Math.sqrt(4) } }
解决类名冲突
当导入的两个包中有同名类时,需通过全限定类名区分,或使用导入别名(JDK 10+ 支持)。
方式 1:全限定类名
package com.demo.test;
// 1. 导入 java.util.Date
import java.util.Date;
public class DateConflictDemo {
public static void main(String[] args) {
// 2. java.util.Date
Date utilDate = new Date();
// 3. 用全限定类名区分 java.sql.Date
java.sql.Date sqlDate = new java.sql.Date(System.currentTimeMillis());
}
}方式 2:导入别名
package com.demo.test;
// 给 java.sql.Date 起别名 SqlDate
import java.sql.Date as SqlDate;
import java.util.Date;
public class DateAliasDemo {
public static void main(String[] args) {
Date utilDate = new Date();
SqlDate sqlDate = new SqlDate(System.currentTimeMillis()); // 使用别名
}
}包的编译与运行
Java 编译器要求包名与目录结构完全一致,否则会编译失败或运行时找不到类。
命令行编译与运行步骤:
步骤 1:创建目录结构
假设包名是
com.demo,需创建目录com/demo,将Hello.java放入该目录:plaintext└── com └── demo └── Hello.java步骤 2:编写代码(Hello.java)
javapackage com.demo; public class Hello { public void sayHello() { System.out.println("Hello, Package!"); } }步骤 3:编译代码(javac 命令)
在根目录(
com目录的上级目录)执行编译命令,使用 :shjavac -d bin com/demo/Hello.java-d bin:指定编译后类文件的输出目录(保持包结构),如bin/com/demo/Hello.class。
步骤 4:运行代码(java 命令)
在根目录执行运行命令,使用全限定类名(包名 + 类名):
shjava -cp bin com.demo.Hello-cp bin:指定类路径为bin目录;com.demo.Hello:全限定类名,不能写Hello或com/demo/Hello。
IDE 包管理
Java 核心包
JDK 提供了大量内置包,包含常用的类和接口,以下是最核心的几个:
java.lang:核心包,自动导入,包含
String、Object、System、Math等类;java.util:工具包,包含
ArrayList、HashMap、Date、Random、Scanner等类;java.io:输入输出包,包含文件操作、流操作相关类(如
File、InputStream);java.net:网络编程包,包含
Socket、ServerSocket等类;java.sql:数据库编程包,包含
Connection、Statement、ResultSet等接口;java.awt/swing:图形界面编程包,包含窗口、按钮等组件类。